Passed
Pull Request — master (#130)
by Jan
01:37
created

ID3Helpers.js ➔ getFramesFromID3Body   F

Complexity

Conditions 17

Size

Total Lines 53
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 37
dl 0
loc 53
rs 1.8
c 0
b 0
f 0
cc 17

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like ID3Helpers.js ➔ getFramesFromID3Body often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
const zlib = require('zlib')
2
const ID3Definitions = require("./ID3Definitions")
3
const ID3Frames = require('./ID3Frames')
4
const ID3Util = require('./ID3Util')
5
6
/**
7
 * Returns array of buffers created by tags specified in the tags argument
8
 * @param tags - Object containing tags to be written
9
 * @returns {Array}
10
 */
11
function createBuffersFromTags(tags) {
12
    const frames = []
13
    if(!tags) {
14
        return frames
15
    }
16
    const rawObject = Object.keys(tags).reduce((acc, val) => {
17
        if(ID3Definitions.FRAME_IDENTIFIERS.v3[val] !== undefined) {
18
            acc[ID3Definitions.FRAME_IDENTIFIERS.v3[val]] = tags[val]
19
        } else if(ID3Definitions.FRAME_IDENTIFIERS.v4[val] !== undefined) {
20
            /**
21
             * Currently, node-id3 always writes ID3 version 3.
22
             * However, version 3 and 4 are very similar, and node-id3 can also read version 4 frames.
23
             * Until version 4 is fully supported, as a workaround, allow writing version 4 frames into a version 3 tag.
24
             * If a reader does not support a v4 frame, it's (per spec) supposed to skip it, so it should not be a problem.
25
             */
26
            acc[ID3Definitions.FRAME_IDENTIFIERS.v4[val]] = tags[val]
27
        } else {
28
            acc[val] = tags[val]
29
        }
30
        return acc
31
    }, {})
32
33
    Object.keys(rawObject).forEach((specName) => {
34
        let frame
35
        // Check if invalid specName
36
        if(specName.length !== 4) {
37
            return
38
        }
39
        if(ID3Frames[specName] !== undefined) {
40
            frame = ID3Frames[specName].create(rawObject[specName], 3)
41
        } else if(specName.startsWith('T')) {
42
            frame = ID3Frames.GENERIC_TEXT.create(specName, rawObject[specName], 3)
43
        } else if(specName.startsWith('W')) {
44
            if(ID3Util.getSpecOptions(specName, 3).multiple && rawObject[specName] instanceof Array && rawObject[specName].length > 0) {
45
                frame = Buffer.alloc(0)
46
                // deduplicate array
47
                for(const url of [...new Set(rawObject[specName])]) {
48
                    frame = Buffer.concat([frame, ID3Frames.GENERIC_URL.create(specName, url, 3)])
49
                }
50
            } else {
51
                frame = ID3Frames.GENERIC_URL.create(specName, rawObject[specName], 3)
52
            }
53
        }
54
55
        if (frame && frame instanceof Buffer) {
56
            frames.push(frame)
57
        }
58
    })
59
60
    return frames
61
}
62
63
/**
64
 * Return a buffer with the frames for the specified tags
65
 * @param tags - Object containing tags to be written
66
 * @returns {Buffer}
67
 */
68
module.exports.createBufferFromTags = function(tags) {
69
    return Buffer.concat(createBuffersFromTags(tags))
70
}
71
72
module.exports.getTagsFromBuffer = function(filebuffer, options) {
73
    const framePosition = ID3Util.getFramePosition(filebuffer)
74
    if(framePosition === -1) {
75
        return getTagsFromFrames([], 3, options)
76
    }
77
    const frameSize = ID3Util.decodeSize(filebuffer.slice(framePosition + 6, framePosition + 10)) + 10
78
    const ID3Frame = Buffer.alloc(frameSize + 1)
79
    filebuffer.copy(ID3Frame, 0, framePosition)
80
    //ID3 version e.g. 3 if ID3v2.3.0
81
    const ID3Version = ID3Frame[3]
82
    const tagFlags = ID3Util.parseTagHeaderFlags(ID3Frame)
83
    let extendedHeaderOffset = 0
84
    if(tagFlags.extendedHeader) {
85
        if(ID3Version === 3) {
86
            extendedHeaderOffset = 4 + filebuffer.readUInt32BE(10)
87
        } else if(ID3Version === 4) {
88
            extendedHeaderOffset = ID3Util.decodeSize(filebuffer.slice(10, 14))
89
        }
90
    }
91
    const ID3FrameBody = Buffer.alloc(frameSize - 10 - extendedHeaderOffset)
92
    filebuffer.copy(ID3FrameBody, 0, framePosition + 10 + extendedHeaderOffset)
93
94
    const frames = getFramesFromID3Body(ID3FrameBody, ID3Version, options)
95
96
    return getTagsFromFrames(frames, ID3Version, options)
97
}
98
99
function getFramesFromID3Body(ID3FrameBody, ID3Version, options = {}) {
100
    let currentPosition = 0
101
    const frames = []
102
    if(!ID3FrameBody || !(ID3FrameBody instanceof Buffer)) {
103
        return frames
104
    }
105
106
    let identifierSize = 4
107
    let textframeHeaderSize = 10
108
    if(ID3Version === 2) {
109
        identifierSize = 3
110
        textframeHeaderSize = 6
111
    }
112
113
    while(currentPosition < ID3FrameBody.length && ID3FrameBody[currentPosition] !== 0x00) {
114
        const bodyFrameHeader = Buffer.alloc(textframeHeaderSize)
115
        ID3FrameBody.copy(bodyFrameHeader, 0, currentPosition)
116
117
        let decodeSize = false
118
        if(ID3Version === 4) {
119
            decodeSize = true
120
        }
121
        let bodyFrameSize = ID3Util.getFrameSize(bodyFrameHeader, decodeSize, ID3Version)
122
        if(bodyFrameSize + 10 > (ID3FrameBody.length - currentPosition)) {
123
            break
124
        }
125
        const specName = bodyFrameHeader.toString('utf8', 0, identifierSize)
126
        if(options.exclude instanceof Array && options.exclude.includes(specName) || options.include instanceof Array && !options.include.includes(specName)) {
127
            currentPosition += bodyFrameSize + textframeHeaderSize
128
            continue
129
        }
130
        const frameHeaderFlags = ID3Util.parseFrameHeaderFlags(bodyFrameHeader, ID3Version)
131
        if(frameHeaderFlags.dataLengthIndicator) {
132
            bodyFrameSize -= 4
133
        }
134
        const bodyFrameBuffer = Buffer.alloc(bodyFrameSize)
135
        ID3FrameBody.copy(bodyFrameBuffer, 0, currentPosition + textframeHeaderSize + (frameHeaderFlags.dataLengthIndicator ? 4 : 0))
136
        const frame = {
137
            name: specName,
138
            flags: frameHeaderFlags,
139
            body: frameHeaderFlags.unsynchronisation ? ID3Util.processUnsynchronisedBuffer(bodyFrameBuffer) : bodyFrameBuffer
140
        }
141
        if(frameHeaderFlags.dataLengthIndicator) {
142
            frame['dataLengthIndicator'] = ID3FrameBody.readInt32BE(currentPosition + textframeHeaderSize)
143
        }
144
        frames.push(frame)
145
146
        //  Size of sub frame + its header
147
        currentPosition += bodyFrameSize + textframeHeaderSize
148
    }
149
150
    return frames
151
}
152
153
function decompressFrame(frame) {
154
    if(frame.body.length < 5 || frame.dataLengthIndicator === undefined) {
155
        return null
156
    }
157
158
    /*
159
    * ID3 spec defines that compression is stored in ZLIB format, but doesn't specify if header is present or not.
160
    * ZLIB has a 2-byte header.
161
    * 1. try if header + body decompression
162
    * 2. else try if header is not stored (assume that all content is deflated "body")
163
    * 3. else try if inflation works if the header is omitted (implementation dependent)
164
    * */
165
    let decompressedBody
166
    try {
167
        decompressedBody = zlib.inflateSync(frame.body)
168
    } catch (e) {
169
        try {
170
            decompressedBody = zlib.inflateRawSync(frame.body)
171
        } catch (e) {
172
            try {
173
                decompressedBody = zlib.inflateRawSync(frame.body.slice(2))
174
            } catch (e) {
175
                return null
176
            }
177
        }
178
    }
179
    if(decompressedBody.length !== frame.dataLengthIndicator) {
180
        return null
181
    }
182
    return decompressedBody
183
}
184
185
function getTagsFromFrames(frames, ID3Version, options = {}) {
186
    const tags = { }
187
    const raw = { }
188
189
    frames.forEach((frame) => {
190
        let specName
191
        let identifier
192
        if(ID3Version === 2) {
193
            specName = ID3Definitions.FRAME_IDENTIFIERS.v3[ID3Definitions.FRAME_INTERNAL_IDENTIFIERS.v2[frame.name]]
194
            identifier = ID3Definitions.FRAME_INTERNAL_IDENTIFIERS.v2[frame.name]
195
        } else if(ID3Version === 3 || ID3Version === 4) {
196
            /**
197
             * Due to their similarity, it's possible to mix v3 and v4 frames even if they don't exist in their corrosponding spec.
198
             * Programs like Mp3tag allow you to do so, so we should allow reading e.g. v4 frames from a v3 ID3 Tag
199
             */
200
            specName = frame.name
201
            identifier = ID3Definitions.FRAME_INTERNAL_IDENTIFIERS.v3[frame.name] || ID3Definitions.FRAME_INTERNAL_IDENTIFIERS.v4[frame.name]
202
        }
203
204
        if(!specName || !identifier || frame.flags.encryption) {
205
            return
206
        }
207
208
        if(frame.flags.compression) {
209
            const decompressedBody = decompressFrame(frame)
210
            if(!decompressedBody) {
211
                return
212
            }
213
            frame.body = decompressedBody
214
        }
215
216
        let decoded
217
        if(ID3Frames[specName]) {
218
            decoded = ID3Frames[specName].read(frame.body, ID3Version)
219
        } else if(specName.startsWith('T')) {
220
            decoded = ID3Frames.GENERIC_TEXT.read(frame.body, ID3Version)
221
        } else if(specName.startsWith('W')) {
222
            decoded = ID3Frames.GENERIC_URL.read(frame.body, ID3Version)
223
        }
224
225
        if(!decoded) {
226
            return
227
        }
228
229
        if(ID3Util.getSpecOptions(specName, ID3Version).multiple) {
230
            if(!options.onlyRaw) {
231
                if(!tags[identifier]) {
232
                    tags[identifier] = []
233
                }
234
                tags[identifier].push(decoded)
235
            }
236
            if(!options.noRaw) {
237
                if(!raw[specName]) {
238
                    raw[specName] = []
239
                }
240
                raw[specName].push(decoded)
241
            }
242
        } else {
243
            if(!options.onlyRaw) {
244
                tags[identifier] = decoded
245
            }
246
            if(!options.noRaw) {
247
                raw[specName] = decoded
248
            }
249
        }
250
    })
251
252
    if(options.onlyRaw) {
253
        return raw
254
    }
255
    if(options.noRaw) {
256
        return tags
257
    }
258
259
    tags.raw = raw
260
    return tags
261
}
262
263
module.exports.getTagsFromID3Body = function(body) {
264
    return getTagsFromFrames(getFramesFromID3Body(body, 3), 3)
265
}
266